"use strict";

exports.__esModule = true;

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

var _maxExpireDate = new Date('Fri, 31 Dec 9999 23:59:59 UTC');
var _defaultExpire = _maxExpireDate;

// https://github.com/jeromegn/Backbone.localStorage/blob/master/backbone.localStorage.js#L63
var defaultSerializer = {
    serialize: function serialize(item) {
        return JSON.stringify(item);
    },
    // fix for "illegal access" error on Android when JSON.parse is
    // passed null
    deserialize: function deserialize(data) {
        return data && JSON.parse(data);
    }
};

function _extend(obj, props) {
    for (var key in props) {
        obj[key] = props[key];
    }return obj;
}

/**
 * https://github.com/gsklee/ngStorage/blob/master/ngStorage.js#L52
 *
 * When Safari (OS X or iOS) is in private browsing mode, it appears as
 * though localStorage is available, but trying to call .setItem throws an
 * exception below: "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was
 * made to add something to storage that exceeded the quota."
 */
function _isStorageSupported(storage) {
    var supported = false;
    if (storage && storage.setItem) {
        supported = true;
        var key = '__' + Math.round(Math.random() * 1e7);
        try {
            storage.setItem(key, key);
            storage.removeItem(key);
        } catch (err) {
            supported = false;
        }
    }
    return supported;
}

// get storage instance
function _getStorageInstance(storage) {
    var type = typeof storage === 'undefined' ? 'undefined' : _typeof(storage);
    if (type === 'string' && window[storage] instanceof Storage) {
        return window[storage];
    }
    return storage;
}

function _isValidDate(date) {
    return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime());
}

function _getExpiresDate(expires, now) {
    now = now || new Date();

    if (typeof expires === 'number') {
        expires = expires === Infinity ? _maxExpireDate : new Date(now.getTime() + expires);
    } else if (typeof expires === 'string') {
        expires = new Date(expires);
    }

    if (expires && !_isValidDate(expires)) {
        throw new Error('`expires` parameter cannot be converted to a valid Date instance');
    }

    return expires;
}

// http://crocodillon.com/blog/always-catch-localstorage-security-and-quota-exceeded-errors
function _isQuotaExceeded(e) {
    var quotaExceeded = false;
    if (e) {
        if (e.code) {
            switch (e.code) {
                case 22:
                    quotaExceeded = true;
                    break;
                case 1014:
                    // Firefox
                    if (e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
                        quotaExceeded = true;
                    }
                    break;
            }
        } else if (e.number === -2147024882) {
            // Internet Explorer 8
            quotaExceeded = true;
        }
    }
    return quotaExceeded;
}

// cache item constructor
function CacheItemConstructor(value, exp) {
    // createTime
    this.c = new Date().getTime();
    exp = exp || _defaultExpire;
    var expires = _getExpiresDate(exp);
    // expiresTime
    this.e = expires.getTime();
    this.v = value;
}

function _isCacheItem(item) {
    if ((typeof item === 'undefined' ? 'undefined' : _typeof(item)) !== 'object') {
        return false;
    }
    if (item) {
        if ('c' in item && 'e' in item && 'v' in item) {
            return true;
        }
    }
    return false;
}

// check cacheItem If effective
function _checkCacheItemIfEffective(cacheItem) {
    var timeNow = new Date().getTime();
    return timeNow < cacheItem.e;
}

function _checkAndWrapKeyAsString(key) {
    if (typeof key !== 'string') {
        console.warn(key + ' used as a key, but it is not a string.');
        key = String(key);
    }
    return key;
}

// cache api
var CacheAPI = {

    set: function set(key, value, options) {},

    get: function get(key) {},

    'delete': function _delete(key) {},
    // Try the best to clean All expires CacheItem.
    deleteAllExpires: function deleteAllExpires() {},
    // Clear all keys
    clear: function clear() {},
    // Add key-value item to memcached, success only when the key is not exists in memcached.
    add: function add(key, options) {},
    // Replace the key's data item in cache, success only when the key's data item is exists in cache.
    replace: function replace(key, value, options) {},
    // Set a new options for an existing key.
    touch: function touch(key, exp) {}
};

// cache api
var CacheAPIImpl = {

    set: function set(key, val, options) {

        key = _checkAndWrapKeyAsString(key);

        options = _extend({ force: true }, options);

        if (val === undefined) {
            return this['delete'](key);
        }

        var value = defaultSerializer.serialize(val);

        var cacheItem = new CacheItemConstructor(value, options.exp);
        try {
            this.storage.setItem(key, defaultSerializer.serialize(cacheItem));
        } catch (e) {
            if (_isQuotaExceeded(e)) {
                //data wasn't successfully saved due to quota exceed so throw an error
                this.quotaExceedHandler(key, value, options, e);
            } else {
                console.error(e);
            }
        }

        return val;
    },
    get: function get(key) {
        key = _checkAndWrapKeyAsString(key);
        var cacheItem = null;
        try {
            cacheItem = defaultSerializer.deserialize(this.storage.getItem(key));
        } catch (e) {
            return null;
        }
        if (_isCacheItem(cacheItem)) {
            if (_checkCacheItemIfEffective(cacheItem)) {
                var value = cacheItem.v;
                return defaultSerializer.deserialize(value);
            } else {
                this['delete'](key);
            }
        }
        return null;
    },

    'delete': function _delete(key) {
        key = _checkAndWrapKeyAsString(key);
        this.storage.removeItem(key);
        return key;
    },

    deleteAllExpires: function deleteAllExpires() {
        var length = this.storage.length;
        var deleteKeys = [];
        var _this = this;
        for (var i = 0; i < length; i++) {
            var key = this.storage.key(i);
            var cacheItem = null;
            try {
                cacheItem = defaultSerializer.deserialize(this.storage.getItem(key));
            } catch (e) {}

            if (cacheItem !== null && cacheItem.e !== undefined) {
                var timeNow = new Date().getTime();
                if (timeNow >= cacheItem.e) {
                    deleteKeys.push(key);
                }
            }
        }
        deleteKeys.forEach(function (key) {
            _this['delete'](key);
        });
        return deleteKeys;
    },

    clear: function clear() {
        this.storage.clear();
    },

    add: function add(key, value, options) {
        key = _checkAndWrapKeyAsString(key);
        options = _extend({ force: true }, options);
        try {
            var cacheItem = defaultSerializer.deserialize(this.storage.getItem(key));
            if (!_isCacheItem(cacheItem) || !_checkCacheItemIfEffective(cacheItem)) {
                this.set(key, value, options);
                return true;
            }
        } catch (e) {
            this.set(key, value, options);
            return true;
        }
        return false;
    },

    replace: function replace(key, value, options) {
        key = _checkAndWrapKeyAsString(key);
        var cacheItem = null;
        try {
            cacheItem = defaultSerializer.deserialize(this.storage.getItem(key));
        } catch (e) {
            return false;
        }
        if (_isCacheItem(cacheItem)) {
            if (_checkCacheItemIfEffective(cacheItem)) {
                this.set(key, value, options);
                return true;
            } else {
                this['delete'](key);
            }
        }
        return false;
    },

    touch: function touch(key, exp) {
        key = _checkAndWrapKeyAsString(key);
        var cacheItem = null;
        try {
            cacheItem = defaultSerializer.deserialize(this.storage.getItem(key));
        } catch (e) {
            return false;
        }
        if (_isCacheItem(cacheItem)) {
            if (_checkCacheItemIfEffective(cacheItem)) {
                this.set(key, this.get(key), { exp: exp });
                return true;
            } else {
                this['delete'](key);
            }
        }
        return false;
    }
};

/**
 * Cache Constructor
 */
function CacheConstructor(options) {

    // default options
    var defaults = {
        storage: 'localStorage',
        exp: Infinity //An expiration time, in seconds. default never .
    };

    var opt = _extend(defaults, options);

    var expires = opt.exp;

    if (expires && typeof expires !== 'number' && !_isValidDate(expires)) {
        throw new Error('Constructor `exp` parameter cannot be converted to a valid Date instance');
    } else {
        _defaultExpire = expires;
    }

    var storage = _getStorageInstance(opt.storage);

    var isSupported = _isStorageSupported(storage);

    this.isSupported = function () {
        return isSupported;
    };

    if (isSupported) {

        this.storage = storage;

        this.quotaExceedHandler = function (key, val, options, e) {
            console.warn('Quota exceeded!');
            if (options && options.force === true) {
                var deleteKeys = this.deleteAllExpires();
                console.warn('delete all expires CacheItem : [' + deleteKeys + '] and try execute `set` method again!');
                try {
                    options.force = false;
                    this.set(key, val, options);
                } catch (err) {
                    console.warn(err);
                }
            }
        };
    } else {
        // if not support, rewrite all functions without doing anything
        _extend(this, CacheAPI);
    }
}

CacheConstructor.prototype = CacheAPIImpl;

exports['default'] = CacheConstructor;